/*
 * Copyright (C) 2018 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:JvmName("KotlinExtensions")

package retrofit2

import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.intercepted
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine

inline fun <reified T : Any> Retrofit.create(): T = create(T::class.java)

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation { cancel() }
    enqueue(
      object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
          if (response.isSuccessful) {
            val body = response.body()
            if (body == null) {
              val invocation = call.request().tag(Invocation::class.java)!!
              val service = invocation.service()
              val method = invocation.method()
              val e =
                KotlinNullPointerException(
                  "Response from ${service.name}.${method.name}" +
                    " was null but response body type was declared as non-null"
                )
              continuation.resumeWithException(e)
            } else {
              continuation.resume(body)
            }
          } else {
            continuation.resumeWithException(HttpException(response))
          }
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
          continuation.resumeWithException(t)
        }
      }
    )
  }
}

@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation { cancel() }
    enqueue(
      object : Callback<T?> {
        override fun onResponse(call: Call<T?>, response: Response<T?>) {
          if (response.isSuccessful) {
            continuation.resume(response.body())
          } else {
            continuation.resumeWithException(HttpException(response))
          }
        }

        override fun onFailure(call: Call<T?>, t: Throwable) {
          continuation.resumeWithException(t)
        }
      }
    )
  }
}

@JvmName("awaitUnit")
suspend fun Call<Unit>.await() {
  @Suppress("UNCHECKED_CAST") (this as Call<Unit?>).await()
}

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation { cancel() }
    enqueue(
      object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
          continuation.resume(response)
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
          continuation.resumeWithException(t)
        }
      }
    )
  }
}

/**
 * Force the calling coroutine to suspend before throwing [this].
 *
 * This is needed when a checked exception is synchronously caught in a [java.lang.reflect.Proxy]
 * invocation to avoid being wrapped in [java.lang.reflect.UndeclaredThrowableException].
 *
 * The implementation is derived from:
 * https://github.com/Kotlin/kotlinx.coroutines/pull/1667#issuecomment-556106349
 */
internal suspend fun Throwable.suspendAndThrow(): Nothing {
  suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
    Dispatchers.Default.dispatch(continuation.context) {
      continuation.intercepted().resumeWithException(this@suspendAndThrow)
    }
    COROUTINE_SUSPENDED
  }
}
